package org.hackystat.sensor.ant.pmd;

import java.io.File;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;
import javax.xml.datatype.XMLGregorianCalendar;

import org.apache.tools.ant.BuildException;
import org.hackystat.sensor.ant.pmd.jaxb.ObjectFactory;
import org.hackystat.sensor.ant.pmd.jaxb.Pmd;
import org.hackystat.sensor.ant.pmd.jaxb.Violation;
import org.hackystat.sensor.ant.task.HackystatSensorTask;
import org.hackystat.sensor.ant.util.LongTimeConverter;
import org.hackystat.sensorshell.SensorShellException;

/**
 * Implements an Ant task that parses the XML files generated by PMD Ant Task and sends the
 * CodeIssue data to a Hackystat server.
 * 
 * @author Aaron A. Kagawa, Julie Ann Sakuda, Philip Johnson
 */
public class PmdSensor extends HackystatSensorTask {

  /** Prefix for type attributes. */
  private static final String TYPE = "Type_";

  /** Tool in UserMap to use. */
  private static String tool = "PMD";

  /** Initialize a new instance of a PmdSensor. */
  public PmdSensor() {
    super(tool);
  }

  /**
   * Initialize a new instance of a PmdSensor, passing the host email, and password directly. This
   * supports testing.
   * 
   * @param host The hackystat host URL.
   * @param email The Hackystat email to use.
   * @param password The Hackystat password to use.
   */
  public PmdSensor(String host, String email, String password) {
    super(host, email, password, tool);
  }

  /**
   * Parses the PMD XML files and sends the resulting code issue results to the hackystat server.
   * This method is invoked automatically by Ant.
   * 
   * @throws BuildException If there is an error.
   */
  @Override
  public void executeInternal() throws BuildException {
    setupSensorShell();
    int numberOfCodeIssues = 0;
    Date startTime = new Date();

    // Iterate though each file, extract the PMD data, send to sensorshell.
    for (File dataFile : getDataFiles()) {
      verboseInfo("Processing file: " + dataFile);
      try {
        numberOfCodeIssues += processPmdXmlFile(dataFile);
      }
      catch (Exception e) {
        signalError("Failure processing: " + dataFile, e);
      }
    }
    this.sendAndQuit();
    summaryInfo(startTime, "Code Issue", numberOfCodeIssues);
  }

  /**
   * Parses a PMD XML file and sends the code issue instances to the shell.
   * 
   * @param xmlFile The XML file name to be processed.
   * @return The number of issues that have been processed in this XML file.
   * @exception BuildException if any error.
   */
  public int processPmdXmlFile(File xmlFile) throws BuildException {
    XMLGregorianCalendar runtimeGregorian = LongTimeConverter.convertLongToGregorian(this.runtime);

    try {
      JAXBContext context = JAXBContext.newInstance(ObjectFactory.class);
      Unmarshaller unmarshaller = context.createUnmarshaller();

      List<File> allSourceFiles = this.getSourceFiles();
      Set<String> filesWithViolations = new HashSet<String>();

      Pmd pmdResults = (Pmd) unmarshaller.unmarshal(xmlFile);
      List<org.hackystat.sensor.ant.pmd.jaxb.File> files = pmdResults.getFile();

      int codeIssueCount = 0;
      verboseInfo("Processing information about files that had PMD issues.");
      for (org.hackystat.sensor.ant.pmd.jaxb.File file : files) {
        // Base unique timestamp off of the runtime (which is when it started running)
        long uniqueTstamp = this.tstampSet.getUniqueTstamp(this.runtime);
        // Get altered time as XMLGregorianCalendar
        XMLGregorianCalendar uniqueTstampGregorian = LongTimeConverter
            .convertLongToGregorian(uniqueTstamp);

        // derive the full path name from the file name
        String fileName = file.getName();

        String fullFilePath = this.findSrcFile(allSourceFiles, fileName);
        filesWithViolations.add(fullFilePath);

        Map<String, String> keyValMap = new HashMap<String, String>();
        // Required
        keyValMap.put("Tool", "PMD");
        keyValMap.put("SensorDataType", "CodeIssue");
        keyValMap.put("Timestamp", uniqueTstampGregorian.toString());
        keyValMap.put("Runtime", runtimeGregorian.toString());
        keyValMap.put("Resource", fullFilePath);

        HashMap<String, Integer> issueCounts = new HashMap<String, Integer>();

        List<Violation> violations = file.getViolation();
        for (Violation violation : violations) {
          String rule = violation.getRule();
          String ruleset = violation.getRuleset();

          String key = ruleset + "_" + rule;
          key = key.replaceAll(" ", ""); // remove spaces
          if (issueCounts.containsKey(key)) {
            Integer count = issueCounts.get(key);
            issueCounts.put(key, ++count);
          }
          else {
            // no previous mapping, add 1st issue to map
            issueCounts.put(key, 1);
          }
        }
        for (Entry<String, Integer> entry : issueCounts.entrySet()) {
          String typeKey = TYPE + entry.getKey();
          keyValMap.put(typeKey, entry.getValue().toString());
        }

        this.sensorShell.add(keyValMap); // add data to sensorshell
        codeIssueCount++;
      }

      // process the zero issues
      verboseInfo("Generating data for files that did not have PMD issues.");
      for (File srcFile : getSourceFiles()) {
        // Skip this entry if we've already processed it above. 
        if (filesWithViolations.contains(srcFile.getAbsolutePath())) {
          continue;
        }
        // Alter startTime to guarantee uniqueness.
        long uniqueTstamp = this.tstampSet.getUniqueTstamp(this.runtime);

        // Get altered time as XMLGregorianCalendar
        XMLGregorianCalendar uniqueTstampGregorian = LongTimeConverter
            .convertLongToGregorian(uniqueTstamp);

        Map<String, String> keyValMap = new HashMap<String, String>();
        keyValMap.put("Tool", "PMD");
        keyValMap.put("SensorDataType", "CodeIssue");
        keyValMap.put("Timestamp", uniqueTstampGregorian.toString());
        keyValMap.put("Runtime", runtimeGregorian.toString());
        keyValMap.put("Resource", srcFile.getAbsolutePath());

        this.sensorShell.add(keyValMap); // add data to sensorshell
        codeIssueCount++;
      }

      return codeIssueCount;
    }
    catch (JAXBException e) {
      throw new BuildException(errMsgPrefix + "Failure in JAXB " + xmlFile, e);
    }
    catch (SensorShellException f) {
      throw new BuildException(errMsgPrefix + "Failure in SensorShell " + xmlFile, f);
    }
  }

  /**
   * Finds the full file path of the source path within the src files collection. For example,
   * srcFiles could contain: [c:\foo\src\org\Foo.java, c:\foo\src\org\Bar.java] and the sourcePath
   * could be org\Foo.java. This method will find and return the full path of the Foo.java file.
   * 
   * @param srcFiles Contains the full path to the files.
   * @param sourcePath Contains a trimmed version of a file path.
   * @return The full file path, or null if the path is not found.
   */
  private String findSrcFile(List<File> srcFiles, String sourcePath) {
    for (File srcFile : srcFiles) {
      if (srcFile == null) {
        continue;
      }
      String srcFilePath = srcFile.getAbsolutePath();
      String alteredSourcePath = sourcePath;
      if (srcFilePath.contains("\\")) {
        alteredSourcePath = sourcePath.replace('/', '\\');
      }
      if (srcFile != null && srcFilePath.contains(alteredSourcePath)) {
        return srcFilePath;
      }
    }
    return null;
  }
}